/*- * See the file LICENSE for redistribution information. * * Copyright (c) 2002-2006 * Sleepycat Software. All rights reserved. * * $Id: LockManager.java,v 1.1 2006/05/06 08:59:07 ckaestne Exp $ */ package com.sleepycat.je.txn; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import com.sleepycat.je.DatabaseException; import com.sleepycat.je.DeadlockException; import com.sleepycat.je.LockStats; import com.sleepycat.je.RunRecoveryException; import com.sleepycat.je.StatsConfig; import com.sleepycat.je.config.EnvironmentParams; import com.sleepycat.je.dbi.DatabaseImpl; import com.sleepycat.je.dbi.DbConfigManager; import com.sleepycat.je.dbi.EnvConfigObserver; import com.sleepycat.je.dbi.EnvironmentImpl; import com.sleepycat.je.dbi.MemoryBudget; import com.sleepycat.je.dbi.RangeRestartException; import com.sleepycat.je.latch.Latch; import com.sleepycat.je.latch.LatchStats; import com.sleepycat.je.latch.LatchSupport; /** * LockManager manages locks. * * Note that locks are counted as taking up part of the JE cache; */ public abstract class LockManager implements EnvConfigObserver { /* * The total memory cost for a lock is the Lock object, plus its entry and * key in the lock hash table. * * The addition and removal of Lock objects, and the corresponding cost of * their hashmap entry and key are tracked through the LockManager. */ static final long TOTAL_LOCK_OVERHEAD = MemoryBudget.LOCK_OVERHEAD + MemoryBudget.HASHMAP_ENTRY_OVERHEAD + MemoryBudget.LONG_OVERHEAD; private static final long REMOVE_TOTAL_LOCK_OVERHEAD = 0 - TOTAL_LOCK_OVERHEAD; protected int nLockTables = 1; protected Latch[] lockTableLatches; private Map[] lockTables; // keyed by nodeId private EnvironmentImpl envImpl; private MemoryBudget memoryBudget; // private Level traceLevel; private long nRequests; // stats: number of time a request was made private long nWaits; // stats: number of time a request blocked private static RangeRestartException rangeRestartException = new RangeRestartException(); private static boolean lockTableDump = false; public LockManager(EnvironmentImpl envImpl) throws DatabaseException { DbConfigManager configMgr = envImpl.getConfigManager(); nLockTables = configMgr.getInt(EnvironmentParams.N_LOCK_TABLES); lockTables = new Map[nLockTables]; lockTableLatches = new Latch[nLockTables]; for (int i = 0; i < nLockTables; i++) { lockTables[i] = new HashMap(); lockTableLatches[i] = LatchSupport.makeLatch("Lock Table " + i, envImpl); } this.envImpl = envImpl; memoryBudget = envImpl.getMemoryBudget(); nRequests = 0; nWaits = 0; /* * traceLevel = Tracer.parseLevel (env, * EnvironmentParams.JE_LOGGING_LEVEL_LOCKMGR); */ /* Initialize mutable properties and register for notifications. */ envConfigUpdate(configMgr); envImpl.addConfigObserver(this); } /** * Process notifications of mutable property changes. */ public void envConfigUpdate(DbConfigManager configMgr) throws DatabaseException { LockInfo.setDeadlockStackTrace(configMgr .getBoolean(EnvironmentParams.TXN_DEADLOCK_STACK_TRACE)); setLockTableDump(configMgr.getBoolean(EnvironmentParams.TXN_DUMPLOCKS)); } /** * Called when the je.txn.dumpLocks property is changed. */ static void setLockTableDump(boolean enable) { lockTableDump = enable; } protected int getLockTableIndex(Long nodeId) { return ((int) nodeId.longValue()) % nLockTables; } protected int getLockTableIndex(long nodeId) { return ((int) nodeId) % nLockTables; } /** * Attempt to acquire a lock of <i>type</i> on <i>nodeId</i>. If the lock * acquisition would result in a deadlock, throw an exception.<br> * If the requested lock is not currently available, block until it is or * until timeout milliseconds have elapsed.<br> * If a lock of <i>type</i> is already held, return EXISTING.<br> * If a WRITE lock is held and a READ lock is requested, return PROMOTION.<br> * * If a lock request is for a lock that is not currently held, return either * NEW or DENIED depending on whether the lock is granted or not.<br> * * @param nodeId * The NodeId to lock. * * @param locker * The Locker to lock this on behalf of. * * @param type * The lock type requested. * * @param timeout * milliseconds to time out after if lock couldn't be obtained. 0 * means block indefinitely. Not used if nonBlockingRequest is * true. * * @param nonBlockingRequest * if true, means don't block if lock can't be acquired, and * ignore the timeout parameter. * * @return a LockGrantType indicating whether the request was fulfilled or * not. LockGrantType.NEW means the lock grant was fulfilled and the * caller did not previously hold the lock. PROMOTION means the lock * was granted and it was a promotion from READ to WRITE. EXISTING * means the lock was already granted (not a promotion). DENIED * means the lock was not granted either because the timeout passed * without acquiring the lock or timeout was -1 and the lock was not * immediately available. * * @throws DeadlockException * if acquiring the lock would result in a deadlock. */ public LockGrantType lock(long nodeId, Locker locker, LockType type, long timeout, boolean nonBlockingRequest, DatabaseImpl database) throws DeadlockException, DatabaseException { assert timeout >= 0; /* * Lock on locker before latching the lockTable to avoid having another * notifier perform the notify before the waiter is actually waiting. */ synchronized (locker) { Long nid = new Long(nodeId); LockAttemptResult result = attemptLock(nid, locker, type, nonBlockingRequest); /* Got the lock, return. */ if (result.success || result.lockGrant == LockGrantType.DENIED) { return result.lockGrant; } assert checkNoLatchesHeld(nonBlockingRequest) : LatchSupport .countLatchesHeld() + " latches held while trying to lock, lock table =" + LatchSupport.latchesHeldToString(); /* * We must have gotten WAIT_* from the lock request. We know that * this is a blocking request, because if it wasn't, Lock.lock would * have returned DENIED. Go wait! */ assert !nonBlockingRequest; try { boolean doWait = true; /* * Before blocking, check locker timeout. We need to check here * or lock timeouts will always take precedence and we'll never * actually get any txn timeouts. */ if (locker.isTimedOut()) { if (validateOwnership(nid, locker, type, true, memoryBudget)) { doWait = false; } else { String errMsg = makeTimeoutMsg("Transaction", locker, nodeId, type, result.lockGrant, result.useLock, locker.getTxnTimeOut(), locker .getTxnStartMillis(), System .currentTimeMillis(), database); throw new DeadlockException(errMsg); } } boolean keepTime = (timeout > 0); long startTime = (keepTime ? System.currentTimeMillis() : 0); while (doWait) { locker.setWaitingFor(result.useLock); try { locker.wait(timeout); } catch (InterruptedException IE) { throw new RunRecoveryException(envImpl, IE); } boolean lockerTimedOut = locker.isTimedOut(); long now = System.currentTimeMillis(); boolean thisLockTimedOut = (keepTime && (now - startTime > timeout)); boolean isRestart = (result.lockGrant == LockGrantType.WAIT_RESTART); /* * Re-check for ownership of the lock following wait. If we * timed out and we don't have ownership then flush this * lock from both the waiters and owners while under the * lock table latch. See SR 10103. */ if (validateOwnership(nid, locker, type, lockerTimedOut || thisLockTimedOut || isRestart, memoryBudget)) { break; } else { /* * After a restart conflict the lock will not be held. */ if (isRestart) { throw rangeRestartException; } if (thisLockTimedOut) { locker.setOnlyAbortable(); String errMsg = makeTimeoutMsg("Lock", locker, nodeId, type, result.lockGrant, result.useLock, timeout, startTime, now, database); throw new DeadlockException(errMsg); } if (lockerTimedOut) { locker.setOnlyAbortable(); String errMsg = makeTimeoutMsg("Transaction", locker, nodeId, type, result.lockGrant, result.useLock, locker.getTxnTimeOut(), locker.getTxnStartMillis(), now, database); throw new DeadlockException(errMsg); } } } } finally { locker.setWaitingFor(null); assert EnvironmentImpl.maybeForceYield(); } locker.addLock(nid, result.useLock, type, result.lockGrant); return result.lockGrant; } } abstract protected LockAttemptResult attemptLock(Long nodeId, Locker locker, LockType type, boolean nonBlockingRequest) throws DatabaseException; protected LockAttemptResult attemptLockInternal(Long nodeId, Locker locker, LockType type, boolean nonBlockingRequest, int lockTableIndex) throws DatabaseException { nRequests++; /* Get the target lock. */ Map lockTable = lockTables[lockTableIndex]; Lock useLock = (Lock) lockTable.get(nodeId); if (useLock == null) { useLock = new Lock(nodeId); lockTable.put(nodeId, useLock); memoryBudget.updateLockMemoryUsage(TOTAL_LOCK_OVERHEAD, lockTableIndex); } /* * Attempt to lock. Possible return values are NEW, PROMOTION, DENIED, * EXISTING, WAIT_NEW, WAIT_PROMOTION, WAIT_RESTART. */ LockGrantType lockGrant = useLock.lock(type, locker, nonBlockingRequest, memoryBudget, lockTableIndex); boolean success = false; /* Was the attempt successful? */ if ((lockGrant == LockGrantType.NEW) || (lockGrant == LockGrantType.PROMOTION)) { locker.addLock(nodeId, useLock, type, lockGrant); success = true; } else if (lockGrant == LockGrantType.EXISTING) { success = true; } else if (lockGrant == LockGrantType.DENIED) { /* Locker.lock will throw LockNotGrantedException. */ } else { nWaits++; } return new LockAttemptResult(useLock, lockGrant, success); } /** * Create a informative lock or txn timeout message. */ protected abstract String makeTimeoutMsg(String lockOrTxn, Locker locker, long nodeId, LockType type, LockGrantType grantType, Lock useLock, long timeout, long start, long now, DatabaseImpl database) throws DatabaseException; /** * Do the real work of creating an lock or txn timeout message. */ protected String makeTimeoutMsgInternal(String lockOrTxn, Locker locker, long nodeId, LockType type, LockGrantType grantType, Lock useLock, long timeout, long start, long now, DatabaseImpl database) { /* * Because we're accessing parts of the lock, need to have protected * access to the lock table because things can be changing out from * underneath us. This is a big hammer to grab for so long while we * traverse the graph, but it's only when we have a deadlock and we're * creating a debugging message. * * The alternative would be to handle ConcurrentModificationExceptions * and retry until none of them happen. */ if (lockTableDump) { System.out.println("++++++++++ begin lock table dump ++++++++++"); for (int i = 0; i < nLockTables; i++) { StringBuffer sb = new StringBuffer(); dumpToStringNoLatch(sb, i); System.out.println(sb.toString()); } System.out.println("++++++++++ end lock table dump ++++++++++"); } StringBuffer sb = new StringBuffer(); sb.append(lockOrTxn); sb.append(" expired. Locker ").append(locker); sb.append(": waited for lock"); if (database != null) { sb.append(" on database=").append(database.getDebugName()); } sb.append(" node=").append(nodeId); sb.append(" type=").append(type); sb.append(" grant=").append(grantType); sb.append(" timeoutMillis=").append(timeout); sb.append(" startTime=").append(start); sb.append(" endTime=").append(now); sb.append("\nOwners: ").append(useLock.getOwnersClone()); sb.append("\nWaiters: ").append(useLock.getWaitersListClone()).append( "\n"); StringBuffer deadlockInfo = findDeadlock(useLock, locker); if (deadlockInfo != null) { sb.append(deadlockInfo); } return sb.toString(); } /** * Release a lock and possibly notify any waiters that they have been * granted the lock. * * @param nodeId * The node ID of the lock to release. * * @return true if the lock is released successfully, false if the lock is * not currently being held. */ boolean release(long nodeId, Locker locker) throws DatabaseException { return release(nodeId, null, locker, true); } /** * Release a lock and possibly notify any waiters that they have been * granted the lock. * * @param lock * The lock to release * * @return true if the lock is released successfully, false if the lock is * not currently being held. */ boolean release(Lock lock, Locker locker) throws DatabaseException { return release(-1, lock, locker, false); } /** * Do the work of releasing a lock and notifying any waiters that they have * been granted the lock. * * @param lock * The lock to release. If null, use nodeId to find lock * @param nodeId * The node ID of the lock to release, if lock is null. May not * be valid if lock is not null. MUST be valid if * removeFromLocker is true * @param locker * @param removeFromLocker * true if we're responsible for * * @return true if the lock is released successfully, false if the lock is * not currently being held. */ private boolean release(long nodeId, Lock lock, Locker locker, boolean removeFromLocker) throws DatabaseException { synchronized (locker) { Set newOwners = releaseAndFindNotifyTargets(nodeId, lock, locker, removeFromLocker); if (newOwners == null) { return false; } if (newOwners.size() > 0) { /* * There is a new set of owners and/or there are restart waiters * that should be notified. */ Iterator iter = newOwners.iterator(); while (iter.hasNext()) { Locker lockerToNotify = (Locker) iter.next(); /* Use notifyAll to support multiple threads per txn. */ synchronized (lockerToNotify) { lockerToNotify.notifyAll(); } assert EnvironmentImpl.maybeForceYield(); } } return true; } } /** * Release the lock, and return the set of new owners to notify, if any. * * @return null if the lock does not exist or the given locker was not the * owner, a non-empty set if owners should be notified after * releasing, an empty set if no notification is required. */ protected abstract Set releaseAndFindNotifyTargets(long nodeId, Lock lock, Locker locker, boolean removeFromLocker) throws DatabaseException; /** * Do the real work of releaseAndFindNotifyTargets */ protected Set releaseAndFindNotifyTargetsInternal(long nodeId, Lock lock, Locker locker, boolean removeFromLocker, int lockTableIndex) throws DatabaseException { Lock useLock = lock; Map lockTable = lockTables[lockTableIndex]; if (useLock == null) { useLock = (Lock) lockTable.get(new Long(nodeId)); } if (useLock == null) { /* Lock doesn't exist. */ return null; } Set lockersToNotify = useLock.release(locker, memoryBudget, lockTableIndex); if (lockersToNotify == null) { /* Not owner. */ return null; } /* * If desired, remove it from the locker's bag. Used when we don't need * to hang onto the lock after release -- like null txns or locks on * deleted LNs. */ if (removeFromLocker) { assert nodeId != -1; locker.removeLock(nodeId, useLock); } /* If it's not in use at all, remove it from the lock table. */ if ((useLock.nWaiters() == 0) && (useLock.nOwners() == 0)) { lockTables[lockTableIndex].remove(useLock.getNodeId()); memoryBudget.updateLockMemoryUsage(REMOVE_TOTAL_LOCK_OVERHEAD, lockTableIndex); } return lockersToNotify; } /** * Transfer ownership a lock from one locker to another locker. We're not * sending any notification to the waiters on the lock table, and the past * and present owner should be ready for the transfer. */ abstract void transfer(long nodeId, Locker owningLocker, Locker destLocker, boolean demoteToRead) throws DatabaseException; /** * Do the real work of transfer */ protected void transferInternal(long nodeId, Locker owningLocker, Locker destLocker, boolean demoteToRead, int lockTableIndex) throws DatabaseException { Map lockTable = lockTables[lockTableIndex]; Lock useLock = (Lock) lockTable.get(new Long(nodeId)); assert useLock != null : "Transfer, lock " + nodeId + " was null"; if (demoteToRead) { useLock.demote(owningLocker); } useLock .transfer(owningLocker, destLocker, memoryBudget, lockTableIndex); owningLocker.removeLock(nodeId, useLock); } /** * Transfer ownership a lock from one locker to a set of other txns, cloning * the lock as necessary. This will always be demoted to read, as we can't * have multiple locker owners any other way. We're not sending any * notification to the waiters on the lock table, and the past and present * owners should be ready for the transfer. */ abstract void transferMultiple(long nodeId, Locker owningLocker, Locker[] destLockers) throws DatabaseException; /** * Do the real work of transferMultiple */ protected void transferMultipleInternal(long nodeId, Locker owningLocker, Locker[] destLockers, int lockTableIndex) throws DatabaseException { Map lockTable = lockTables[lockTableIndex]; Lock useLock = (Lock) lockTable.get(new Long(nodeId)); assert useLock != null : "Transfer, lock " + nodeId + " was null"; useLock.demote(owningLocker); useLock.transferMultiple(owningLocker, destLockers, memoryBudget, lockTableIndex); owningLocker.removeLock(nodeId, useLock); } /** * Demote a lock from write to read. Call back to the owning locker to move * this to its read collection. * * @param lock * The lock to release. If null, use nodeId to find lock * @param locker */ abstract void demote(long nodeId, Locker locker) throws DatabaseException; /** * Do the real work of demote. */ protected void demoteInternal(long nodeId, Locker locker, int lockTableIndex) throws DatabaseException { Map lockTable = lockTables[lockTableIndex]; Lock useLock = (Lock) lockTable.get(new Long(nodeId)); useLock.demote(locker); locker.moveWriteToReadLock(nodeId, useLock); } /** * Test the status of the lock on nodeId. If any transaction holds any lock * on it, true is returned. If no transaction holds a lock on it, false is * returned. * * This method is only used by unit tests. * * @param nodeId * The NodeId to check. * @return true if any transaction holds any lock on the nodeid. false if no * lock is held by any transaction. */ abstract boolean isLocked(Long nodeId) throws DatabaseException; /** * Do the real work of isLocked. */ protected boolean isLockedInternal(Long nodeId, int lockTableIndex) { Map lockTable = lockTables[lockTableIndex]; Lock entry = (Lock) lockTable.get(nodeId); if (entry == null) { return false; } return entry.nOwners() != 0; } /** * Return true if this locker owns this a lock of this type on given node. * * This method is only used by unit tests. */ abstract boolean isOwner(Long nodeId, Locker locker, LockType type) throws DatabaseException; /** * Do the real work of isOwner. */ protected boolean isOwnerInternal(Long nodeId, Locker locker, LockType type, int lockTableIndex) { Map lockTable = lockTables[lockTableIndex]; Lock entry = (Lock) lockTable.get(nodeId); if (entry == null) { return false; } return entry.isOwner(locker, type); } /** * Return true if this locker is waiting on this lock. * * This method is only used by unit tests. */ abstract boolean isWaiter(Long nodeId, Locker locker) throws DatabaseException; /** * Do the real work of isWaiter. */ protected boolean isWaiterInternal(Long nodeId, Locker locker, int lockTableIndex) { Map lockTable = lockTables[lockTableIndex]; Lock entry = (Lock) lockTable.get(nodeId); if (entry == null) { return false; } return entry.isWaiter(locker); } /** * Return the number of waiters for this lock. */ abstract int nWaiters(Long nodeId) throws DatabaseException; /** * Do the real work of nWaiters. */ protected int nWaitersInternal(Long nodeId, int lockTableIndex) { Map lockTable = lockTables[lockTableIndex]; Lock entry = (Lock) lockTable.get(nodeId); if (entry == null) { return -1; } return entry.nWaiters(); } /** * Return the number of owners of this lock. */ abstract int nOwners(Long nodeId) throws DatabaseException; /** * Do the real work of nWaiters. */ protected int nOwnersInternal(Long nodeId, int lockTableIndex) { Map lockTable = lockTables[lockTableIndex]; Lock entry = (Lock) lockTable.get(nodeId); if (entry == null) { return -1; } return entry.nOwners(); } /** * @return the transaction that owns the write lock for this */ abstract Locker getWriteOwnerLocker(Long nodeId) throws DatabaseException; /** * Do the real work of getWriteOwnerLocker. */ protected Locker getWriteOwnerLockerInternal(Long nodeId, int lockTableIndex) throws DatabaseException { Map lockTable = lockTables[lockTableIndex]; Lock lock = (Lock) lockTable.get(nodeId); if (lock == null) { return null; } else if (lock.nOwners() > 1) { /* not a write lock */ return null; } else { return lock.getWriteOwnerLocker(); } } /* * Check if we got ownership while we were waiting. If we didn't get * ownership, and we timed out, remove this locker from the set of waiters. * Do this in a critical section to prevent any orphaning of the lock -- we * must be in a critical section between the time that we check ownership * and when we flush any waiters (SR #10103) @return true if you are the * owner. */ abstract protected boolean validateOwnership(Long nodeId, Locker locker, LockType type, boolean flushFromWaiters, MemoryBudget mb) throws DatabaseException; /* * Do the real work of validateOwnershipInternal. */ protected boolean validateOwnershipInternal(Long nodeId, Locker locker, LockType type, boolean flushFromWaiters, MemoryBudget mb, int lockTableIndex) throws DatabaseException { if (isOwnerInternal(nodeId, locker, type, lockTableIndex)) { return true; } if (flushFromWaiters) { Lock entry = (Lock) lockTables[lockTableIndex].get(nodeId); if (entry != null) { entry.flushWaiter(locker, mb, lockTableIndex); } } return false; } /** * Statistics */ public LockStats lockStat(StatsConfig config) throws DatabaseException { LockStats stats = new LockStats(); stats.setNRequests(nRequests); stats.setNWaits(nWaits); if (config.getClear()) { nWaits = 0; nRequests = 0; } for (int i = 0; i < nLockTables; i++) { LatchStats latchStats = (LatchStats) lockTableLatches[i] .getLatchStats(); stats.accumulateLockTableLatchStats(latchStats); } /* Dump info about the lock table. */ if (!config.getFast()) { dumpLockTable(stats); } return stats; } /** * Dump the lock table to the lock stats. */ abstract protected void dumpLockTable(LockStats stats) throws DatabaseException; /** * Do the real work of dumpLockTableInternal. */ protected void dumpLockTableInternal(LockStats stats, int i) { Map lockTable = lockTables[i]; stats.accumulateNTotalLocks(lockTable.size()); Iterator iter = lockTable.values().iterator(); while (iter.hasNext()) { Lock lock = (Lock) iter.next(); stats.setNWaiters(stats.getNWaiters() + lock.nWaiters()); stats.setNOwners(stats.getNOwners() + lock.nOwners()); /* Go through all the owners for a lock. */ Iterator ownerIter = lock.getOwnersClone().iterator(); while (ownerIter.hasNext()) { LockInfo info = (LockInfo) ownerIter.next(); if (info.getLockType().isWriteLock()) { stats.setNWriteLocks(stats.getNWriteLocks() + 1); } else { stats.setNReadLocks(stats.getNReadLocks() + 1); } } } } /** * Debugging */ public void dump() throws DatabaseException { System.out.println(dumpToString()); } public String dumpToString() throws DatabaseException { StringBuffer sb = new StringBuffer(); for (int i = 0; i < nLockTables; i++) { lockTableLatches[i].acquire(); try { dumpToStringNoLatch(sb, i); } finally { lockTableLatches[i].release(); } } return sb.toString(); } private void dumpToStringNoLatch(StringBuffer sb, int whichTable) { Map lockTable = lockTables[whichTable]; Iterator entries = lockTable.entrySet().iterator(); while (entries.hasNext()) { Map.Entry entry = (Map.Entry) entries.next(); Long nid = (Long) entry.getKey(); Lock lock = (Lock) entry.getValue(); sb.append("---- Node Id: ").append(nid).append("----\n"); sb.append(lock); sb.append('\n'); } } private boolean checkNoLatchesHeld(boolean nonBlockingRequest) { if (nonBlockingRequest) { return true; // don't check if it's a non blocking request. } else { return (LatchSupport.countLatchesHeld() == 0); } } private StringBuffer findDeadlock(Lock lock, Locker rootLocker) { Set ownerSet = new HashSet(); ownerSet.add(rootLocker); StringBuffer ret = findDeadlock1(ownerSet, lock, rootLocker); if (ret != null) { return ret; } else { return null; } } private StringBuffer findDeadlock1(Set ownerSet, Lock lock, Locker rootLocker) { Iterator ownerIter = lock.getOwnersClone().iterator(); while (ownerIter.hasNext()) { LockInfo info = (LockInfo) ownerIter.next(); Locker locker = info.getLocker(); Lock waitsFor = locker.getWaitingFor(); if (ownerSet.contains(locker) || locker == rootLocker) { /* Found a cycle. */ StringBuffer ret = new StringBuffer(); ret.append("Transaction ").append(locker.toString()); ret.append(" owns ").append(lock.getNodeId()); ret.append(" ").append(info).append("\n"); ret.append("Transaction ").append(locker.toString()); ret.append(" waits for "); if (waitsFor == null) { ret.append(" nothing"); } else { ret.append(" node "); ret.append(waitsFor.getNodeId()); } ret.append("\n"); return ret; } if (waitsFor != null) { ownerSet.add(locker); StringBuffer sb = findDeadlock1(ownerSet, waitsFor, rootLocker); if (sb != null) { String waitInfo = "Transaction " + locker + " waits for node " + waitsFor.getNodeId() + "\n"; sb.insert(0, waitInfo); return sb; } ownerSet.remove(locker); // is this necessary? } } return null; } /** * This is just a struct to hold a multi-value return. */ static class LockAttemptResult { boolean success; Lock useLock; LockGrantType lockGrant; LockAttemptResult(Lock useLock, LockGrantType lockGrant, boolean success) { this.useLock = useLock; this.lockGrant = lockGrant; this.success = success; } } }